agentmux_srv\backend\storage\filestore/
ijson.rs

1#![allow(dead_code)]
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! FileStore IJson (incremental JSON) operations.
6
7
8use super::core::FileStore;
9use super::types::FileMeta;
10use crate::backend::storage::error::StoreError;
11
12impl FileStore {
13    /// IJson metadata key: number of commands since last compaction.
14    const IJSON_NUM_COMMANDS: &'static str = "ijson:numcmds";
15    /// IJson metadata key: incremental bytes since last compaction.
16    const IJSON_INC_BYTES: &'static str = "ijson:incbytes";
17
18    /// Compaction threshold: high command count.
19    const IJSON_HIGH_COMMANDS: i64 = 100;
20    /// Compaction threshold: high ratio (incremental/file >= 3x).
21    const IJSON_HIGH_RATIO: f64 = 3.0;
22    /// Compaction threshold: low command count.
23    const IJSON_LOW_COMMANDS: i64 = 10;
24    /// Compaction threshold: low ratio (incremental/file >= 1x).
25    const IJSON_LOW_RATIO: f64 = 1.0;
26
27    /// Append an IJson command to a file. Triggers compaction if thresholds are exceeded.
28    pub fn append_ijson(
29        &self,
30        zone_id: &str,
31        name: &str,
32        command: &serde_json::Value,
33    ) -> Result<(), StoreError> {
34        let file = self.stat(zone_id, name)?.ok_or(StoreError::NotFound)?;
35        if !file.opts.ijson {
36            return Err(StoreError::Other("file is not ijson".to_string()));
37        }
38
39        let cmd_bytes = serde_json::to_string(command)?;
40        let data = format!("{}\n", cmd_bytes);
41        self.append_data(zone_id, name, data.as_bytes())?;
42
43        // Update IJson metadata counters
44        let file = self.stat(zone_id, name)?.ok_or(StoreError::NotFound)?;
45        let mut meta = file.meta.clone();
46
47        let num_cmds = meta
48            .get(Self::IJSON_NUM_COMMANDS)
49            .and_then(|v| v.as_i64())
50            .unwrap_or(0)
51            + 1;
52        let inc_bytes = meta
53            .get(Self::IJSON_INC_BYTES)
54            .and_then(|v| v.as_i64())
55            .unwrap_or(0)
56            + data.len() as i64;
57
58        meta.insert(
59            Self::IJSON_NUM_COMMANDS.to_string(),
60            serde_json::json!(num_cmds),
61        );
62        meta.insert(
63            Self::IJSON_INC_BYTES.to_string(),
64            serde_json::json!(inc_bytes),
65        );
66        self.write_meta(zone_id, name, meta, false)?;
67
68        // Check compaction thresholds
69        let file_size = file.size.max(1); // avoid division by zero
70        let ratio = inc_bytes as f64 / file_size as f64;
71
72        let should_compact = num_cmds > Self::IJSON_HIGH_COMMANDS
73            || ratio >= Self::IJSON_HIGH_RATIO
74            || (num_cmds > Self::IJSON_LOW_COMMANDS && ratio >= Self::IJSON_LOW_RATIO);
75
76        if should_compact {
77            let _ = self.compact_ijson(zone_id, name);
78        }
79
80        Ok(())
81    }
82
83    /// Compact an IJson file: apply all incremental commands to build compacted state,
84    /// then replace file contents with the compacted result.
85    pub fn compact_ijson(
86        &self,
87        zone_id: &str,
88        name: &str,
89    ) -> Result<(), StoreError> {
90        // Read full file
91        let data = self
92            .read_file(zone_id, name)?
93            .ok_or(StoreError::NotFound)?;
94
95        let content = String::from_utf8(data)
96            .map_err(|e| StoreError::Other(format!("invalid utf-8 in ijson file: {}", e)))?;
97
98        // Use the ijson module's compact function
99        let compacted = crate::backend::ijson::compact_ijson(&content)
100            .map_err(|e| StoreError::Other(format!("ijson compact error: {}", e)))?;
101
102        // The compacted result is a single JSON command (set at root).
103        // Write it as the new file content.
104        let compacted_with_newline = format!("{}\n", compacted);
105        self.write_file(zone_id, name, compacted_with_newline.as_bytes())?;
106
107        // Reset IJson counters
108        let mut meta = FileMeta::new();
109        meta.insert(
110            Self::IJSON_NUM_COMMANDS.to_string(),
111            serde_json::json!(0),
112        );
113        meta.insert(
114            Self::IJSON_INC_BYTES.to_string(),
115            serde_json::json!(0),
116        );
117        self.write_meta(zone_id, name, meta, true)?;
118
119        Ok(())
120    }
121}